iT邦幫忙

2025 iThome 鐵人賽

DAY 4
0

延續 Day 3 的 Eq/Field/And,本篇把日常最常見、也是最容易出錯的進階運算子一次講清楚:$gt$gte$lt$lte$in$nin$elemMatch,以及「Array 欄位的自動包裝」行為

常見進階運算子(最小可執行範例)

require 'mongory'
Mongory.enable_symbol_snippets!
Mongory.register(Array)

records = [
  { 'name' => 'Jack', 'age' => 18, 'status' => 'active',   'tags' => [%w(ruby lv6),  %w(rails lv3)] },
  { 'name' => 'Jill', 'age' => 15, 'status' => 'pending',  'tags' => [%w(go lv5)] },
  { 'name' => 'Bob',  'age' => 21, 'status' => 'inactive', 'tags' => [%w(ruby lv8)] }
]

# 1) $gt / $gte / $lt / $lte:數值比較
puts "-- gt";  p records.mongory.where(:age.gt => 18).pluck("name")
# ["Bob"]
puts "-- gte"; p records.mongory.where(:age.gte => 18).pluck("name")
# ["Jack", "Bob"]
puts "-- lt";  p records.mongory.where(:age.lt => 18).pluck("name")
# ["Jill"]
puts "-- lte"; p records.mongory.where(:age.lte => 18).pluck("name")
# ["Jack", "Jill"]

# 2) $in / $nin:集合包含(注意:條件必須是 Array)
puts "-- in";  p records.mongory.where(:status.in => %w(active pending)).pluck("name")
# ["Jack", "Jill"]
puts "-- nin"; p records.mongory.where(:status.nin => %w(active pending)).pluck("name")
# ["Bob"]

# 3) $elemMatch:陣列元素匹配(巢狀條件)
puts "-- elemMatch";
p records.mongory
  .where(:tags.elem_match => { 0 => 'ruby' }) # 以簡單示例表示第一個欄位含 'ruby'
  .pluck("name")

# ["Jack", "Bob"]

筆者建議搭配 explain 驗證語意是否如預期:

q = records.mongory.where(:age.gte => 18, :status.in => %w[active pending])
q.explain

輸出:

https://ithelp.ithome.com.tw/upload/images/20250901/20151038H8RXeiz6wq.png

Array 欄位的「自動包裝」:單值 → $elemMatch

MongoDB 語意下,若欄位本身是 Array (or embeds many),且條件是「單值」,會自動用 $elemMatch 轉為「元素存在即可」
Mongory 也遵循同樣邏輯

records = [
  { 'tags' => %w[ruby lv6] },
  { 'tags' => %w[go lv5] }
]

records.mongory.where(:tags => 'ruby').each { |r| p r } # 等價於 :tags.elem_match => { :$eq => 'ruby' }

explain 會顯示陣列欄位被包裝為 ElemMatch 的樹:

Field: "tags" to match: {"$elemMatch"=>"ruby"}
└─ ElemMatch: "ruby"
   └─ Eq: "ruby"
# ElemMatch + Eq 其實就是 array.include?(x) 的意思

$in/$nin 對「Array 欄位」不是 includes?,而是「交集」

若欄位是 Array,$in 會做「交集是否非空」,$nin 會做「交集是否為空」

records = [ { 'tags' => %w[a e] } ]

# $in: 任一元素命中即可
cond = { :tags.in => %w[a b c] }
p records.mongory.where(cond).to_a.any? # => true(a 命中)

# $nin: 所有元素都不在條件集合內才算通過
cond = { :tags.nin => %w[a b c] }
p records.mongory.where(cond).to_a.any? # => false(a 擋下)

容易踩雷(實務清單)

  • $in/$nin 的條件必須是 Array(否則會觸發型別錯誤)

  • $elemMatch 的條件必須是 Hash;其內部可再接其它運算子(例如 :priority.gt => 5

  • 條件式若遇到欄位是 Array 則會自動將條件包裝在 $elemMatch

  • 空集合語意要留心:$in: [] 通常不會匹配任何東西;$nin: [] 等同不過濾

  • 正規表達式與字串比較的差異:/J/"J" 語意不同,Regex 能命中部分字串,Eq 則需全等

小抄

  • $gt/$gte/$lt/$lte:數值比較,gte/lte 含邊界

  • $in/$nin:集合比較;欄位為 Array 時採交集語意

  • $elemMatch:針對 Array 之元素逐一匹配

  • 單值條件遇到 Array 欄位時會自動用 $elemMatch 包裝條件

完整範例(綜合)

require 'mongory'
Mongory.enable_symbol_snippets!
Mongory.register(Array)

users = [
  { 'name' => 'Ann',  'age' => 19, 'status' => 'active',  'tags' => [{ 'name' => 'ruby',  'priority' => 6 }] },
  { 'name' => 'Ben',  'age' => 22, 'status' => 'pending', 'tags' => [{ 'name' => 'rails', 'priority' => 3 }] },
  { 'name' => 'Cody', 'age' => 17, 'status' => 'active',  'tags' => [{ 'name' => 'ruby',  'priority' => 8 }] }
]

q = users.mongory
  .where(:age.gte => 18)
  .where(:status.in => %w[active pending])
  .where(:tags.elem_match => { :name => 'ruby', :priority.gt => 5 })

q.explain
q.each { |u| p u['name'] } # 預期:只會有 Ann(Cody age < 18 不會命中; Ben tags name 是 rails 也不通過)

筆者在專案實戰裡,會固定以 explain 確認運算子樹形結構是否與心智模型一致,再以小型資料集跑 trace 驗證逐步匹配過程;這能有效降低條件錯配與維護成本

接下來 Day 5 會詳細說明「單子條件解包與層級消除」是什麼

專案首頁(Ruby 版)


上一篇
Day 3:AST 與 matcher tree 基本結構
下一篇
Day 5:單子條件解包與層級消除:性能與可讀性
系列文
Mongory:打造跨語言、高效能的萬用查詢引擎29
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言